﻿using System;
using System.Collections.Generic;
using System.Reflection;
using ApplicationGenerator.BusinessModule.MetaElements;
using ApplicationGenerator.BusinessModule.Interfaces;

namespace ApplicationGenerator.BusinessModule.Elements
{
    /// <summary>
    /// Model
    /// </summary>
    internal class Model : IModel
    {
        /// <summary>
        /// Instancja danego elementu
        /// </summary>
        public object ModelInstance
        {
            get;
            private set;
        }

        /// <summary>
        /// Metamodel powiązany z danym modelem
        /// </summary>
        public IMetaModel MetaModel
        {
            get;
            private set;
        }

        /// <summary>
        /// Konstruktor
        /// </summary>
        /// <param name="metaAdapter">Metamodel powiązany z danym modelem</param>
        /// <param name="elementInstance">Instancja konkretnego modelu</param>
        public Model(IMetaModel metaAdapter, object elementInstance)
        {
            MetaModel = metaAdapter;
            ModelInstance = elementInstance;
        }

        /// <summary>
        /// Ustawia właściwość na danej instancji
        /// </summary>
        /// <param name="propertyName">Nazwa właściwości do ustawienia</param>
        /// <param name="propertyValue">Wartość, która ma zostać nadana danej właściwości</param>
        public void SetProperty(string propertyName, object propertyValue)
        {
            PropertyInfo propertyInfo = MetaModel.ModelType.GetProperty(propertyName);
            if (propertyInfo == null)
            {
                throw new ArgumentException("Model " + MetaModel.ModelName + " nie posiada pola o nazwie " + propertyName);
            }

            try
            {
                propertyInfo.SetValue(ModelInstance, propertyValue, null);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można ustawić własności " + propertyName + "obiektu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }
        }

        /// <summary>
        /// Pobiera wartość danej właściwości
        /// </summary>
        /// <param name="propertyName">Nazwa właściwości do pobrania</param>
        /// <returns>Wartość danej właściwości</returns>
        public object GetProperty(string propertyName)
        {
            PropertyInfo propertyInfo = MetaModel.ModelType.GetProperty(propertyName);
            if (propertyInfo == null)
            {
                throw new ArgumentException("Model " + MetaModel.ModelName + "nie posiada pola o nazwie " + propertyName);
            }

            object propertyValue = null;
            try
            {
                propertyValue = propertyInfo.GetValue(ModelInstance, null);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można pobrać własności " + propertyName + " obiektu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }

            return propertyValue;

        }

        /// <summary>
        /// Ustawia wartość słownikową w danej instancji
        /// </summary>
        /// <param name="dictionaryName">Nazwa właściwości słownikowej</param>
        /// <param name="dictionaryValue">Wartość słownikowa</param>
        public void SetDictionaryValue(string dictionaryName, string dictionaryValue)
        {
            if (MetaModel.MetaFieldCollection[dictionaryName] == null)
            {
                throw new Exception("Brak pola " + dictionaryName + " w modelu " + MetaModel.ModelName);
            }

            if (MetaModel.MetaFieldCollection[dictionaryName].MetaFieldType != MetaFieldType.Dictionary)
            {
                throw new Exception("Pole " + dictionaryName + " w modelu " + MetaModel.ModelName + " nie jest polem słownikowym");
            }

            IMetaModelFacade metaModelFacade = (new FacadeFactory()).CreateMetaModelFacade();
            if (!metaModelFacade.MetaDictionaryCollection.Contains(MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name))
            {
                throw new Exception("Nie określono typu słownikowego " + MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name + " dla pola " + dictionaryName + " w modelu " + MetaModel.ModelName);
            }

            IMetaDictionary metaDictionary = metaModelFacade.MetaDictionaryCollection[MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name];
            string dictionaryKey = metaDictionary.MetaDictionaryItems.GetKeyByValue(dictionaryValue);
            if (dictionaryKey == null)
            {
                throw new Exception("Typ słownikowy " + MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name + " nie posiada klucza " + dictionaryValue);
            }

            SetProperty(dictionaryName, Enum.Parse(MetaModel.MetaFieldCollection[dictionaryName].FieldType, dictionaryKey));

        }

        /// <summary>
        /// Pobiera wartość słownikową
        /// </summary>
        /// <param name="dictionaryName">Nazwa właściwości słownikowej</param>
        /// <returns></returns>
        public string GetDictionaryValue(string dictionaryName)
        {
            if (MetaModel.MetaFieldCollection[dictionaryName] == null)
            {
                throw new Exception("Brak pola " + dictionaryName + " w modelu " + MetaModel.ModelName);
            }

            if (MetaModel.MetaFieldCollection[dictionaryName].MetaFieldType != MetaFieldType.Dictionary)
            {
                throw new Exception("Pole " + dictionaryName + " w modelu " + MetaModel.ModelName + " nie jest polem słownikowym");
            }

            IMetaModelFacade metaModelFacade = (new FacadeFactory()).CreateMetaModelFacade();
            if (!metaModelFacade.MetaDictionaryCollection.Contains(MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name))
            {
                throw new Exception("Nie określono typu słownikowego " + MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name + " dla pola " + dictionaryName + " w modelu " + MetaModel.ModelName);
            }

            IMetaDictionary metaDictionary = metaModelFacade.MetaDictionaryCollection[MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name];
            object dictionaryValue = GetProperty(dictionaryName);
            if (dictionaryValue == null)
            {
                return null;
            }

            string dictionaryKey = Enum.GetName(MetaModel.MetaFieldCollection[dictionaryName].FieldType, dictionaryValue);

            return metaModelFacade.MetaDictionaryCollection[MetaModel.MetaFieldCollection[dictionaryName].FieldType.Name].MetaDictionaryItems[dictionaryKey];
        }

        /// <summary>
        /// Ustawia relację w danym modelu
        /// </summary>
        /// <param name="relationName">Nazwa właściwości z obiektem relacji</param>
        /// <param name="relation">Obiekt modelu relacji</param>
        public void SetRelation(string relationName, IModel relation)
        {
            try
            {
                SetProperty(relationName, relation.ModelInstance);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można ustawić relacji " + relationName + " modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }
        }

        /// <summary>
        /// Pobiera obiekt relacyjny modelu
        /// </summary>
        /// <param name="relationName">Nazwa właściwości z obiektem relacji</param>
        /// <returns>Obiekt relacji</returns>
        public IModel GetRelation(string relationName)
        {
            object relationValue = null;

            try
            {
                relationValue = GetProperty(relationName);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można pobrać relacji " + relationName + " modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }

            IMetaField metaField = MetaModel.MetaFieldCollection[relationName];
            if (metaField == null)
            {
                throw new Exception("Nie można pobrać relacji " + relationName + " modelu " + MetaModel.ModelName);
            }

            IMetaModelFacade metaModelFacade = (new FacadeFactory()).CreateMetaModelFacade();
            IMetaModel metaAdapter = metaModelFacade.MetaModelCollection[metaField.FieldType.Name];
            if (metaAdapter == null)
            {
                throw new Exception("Nie można pobrać definicji modelu " + metaField.FieldType.Name + " dla " + MetaModel.ModelName);
            }

            return new Model(metaAdapter, relationValue);
        }

        /// <summary>
        /// Dodaje obiekt modelu do listy modeli
        /// </summary>
        /// <param name="listName">Nazwa właściwości z listą modeli</param>
        /// <param name="listElement">Obiekt modelu</param>
        public void AddToList(string listName, IModel listElement)
        {
            object listObject = GetProperty(listName);
            Type listType = listObject.GetType();
            MethodInfo listAddMethod = listType.GetMethod("Add");

            try
            {
                listAddMethod.Invoke(listObject, new object[] { listElement.ModelInstance });
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można dodać elementu do listy " + listName + " w modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }
        }

        /// <summary>
        /// Dodaje listę obiektów modeli do listy modeli
        /// </summary>
        /// <param name="listName">Nazwa właściwości z listą modeli</param>
        /// <param name="listOfElements">Lista obiektów modeli</param>
        public void AddToList(string listName, IList<IModel> listOfElements)
        {
            foreach (Model element in listOfElements)
            {
                AddToList(listName, element);
            }
        }

        /// <summary>
        /// Usuwa obiekt modelu z listy modeli
        /// </summary>
        /// <param name="listName">Nazwa właściwości z listą modeli</param>
        /// <param name="listElement">Obiekt modelu</param>
        public void RemoveFromList(string listName, IModel listElement)
        {
            object listObject = GetProperty(listName);
            Type listType = listObject.GetType();
            MethodInfo listRemoveMethod = listType.GetMethod("Remove");

            try
            {
                listRemoveMethod.Invoke(listObject, new object[] { listElement.ModelInstance });
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można usunąć elementu do listy " + listName + " w modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }
        }

        /// <summary>
        /// Pobiera obiekt modelu z listy modeli
        /// </summary>
        /// <param name="listName">Nazwa właściwości z listą modeli</param>
        /// <param name="elementIndex">Indeks modelu do pobrania</param>
        /// <returns>Obiekt modelu</returns>
        public IModel GetListElement(string listName, int elementIndex)
        {
            IMetaField metaField = MetaModel.MetaFieldCollection[listName];
            if (metaField.MetaFieldType != MetaFieldType.List)
            {
                throw new Exception("Pole " + listName + " nie jest listą w modelu " + MetaModel.ModelName);
            }

            object listOfElements = null;
            try
            {
                listOfElements = GetProperty(listName);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można pobrać listy " + listName + " modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }

            if (listOfElements == null)
            {
                throw new Exception("Nie można pobrać listy " + listName + " modelu " + MetaModel.ModelName);
            }


            Type listType = listOfElements.GetType();
            PropertyInfo listItemProperty = listType.GetProperty("Item");
            if (listItemProperty == null)
            {
                throw new Exception("Nie można pobrać listy " + listName + " w modelu " + MetaModel.ModelName);
            }

            object listElement = null;
            try
            {
                listElement = listItemProperty.GetValue(listOfElements, new object[] { elementIndex });
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można pobrać elementu " + elementIndex + " na liście " + listName + " w modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }

            if (listElement == null)
            {
                return null;
            }

            Type listElementType = listElement.GetType();
            IMetaModelFacade metaModelFacade = (new FacadeFactory()).CreateMetaModelFacade();
            IMetaModel metaAdapter = metaModelFacade.MetaModelCollection[listElementType.Name];
            if (metaAdapter == null)
            {
                throw new Exception("Nie można pobrać definicji modelu " + metaField.FieldType.Name + " dla " + MetaModel.ModelName);
            }

            return new Model(metaAdapter, listElement);
        }

        /// <summary>
        /// Pobiera liczbę modeli z listy
        /// </summary>
        /// <param name="listName">Nazwa właściwości z listą modeli</param>
        /// <returns></returns>
        public int GetListCount(string listName)
        {
            object listValue = null;

            try
            {
                listValue = GetProperty(listName);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można pobrać listy " + listName + " modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }

            if (listValue == null)
            {
                return 0;
            }


            Type listType = listValue.GetType();
            PropertyInfo listCountProperty = listType.GetProperty("Count");

            if (listCountProperty == null)
            {
                throw new Exception("Nie można pobrać liczby elementów listy " + listName + " w modelu " + MetaModel.ModelName);
            }

            object listElementNumber = null;
            try
            {
                listElementNumber = listCountProperty.GetValue(listValue, null);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można pobrać liczby elementów na liście " + listName + " w modelu " + MetaModel.ModelName + ". Powód: " + ex.Message);
            }

            if (listElementNumber == null || !(listElementNumber is int))
            {
                return 0;
            }

            return (int)listElementNumber;
        }

        /// <summary>
        /// Pobiera listę modeli
        /// </summary>
        /// <param name="listName">Nazwa właściwości z listą modeli</param>
        /// <returns>Lista modeli</returns>
        public IList<IModel> GetList(string listName)
        {
            IList<IModel> elements = new List<IModel>();
            int elementsOnList = GetListCount(listName);

            for (int counter = 0; counter < elementsOnList; counter++)
            {
                elements.Add(GetListElement(listName, counter));
            }

            return elements;
        }

        /// <summary>
        /// Odświeża właściwości modelu
        /// </summary>
        public void Refresh()
        {
            MethodInfo saveMethod = MetaModel.ModelType.GetMethod("Refresh");
            try
            {
                saveMethod.Invoke(ModelInstance, null);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można odświeżyć elementu. Powód: " + ex.Message);
            }
        }

        /// <summary>
        /// Usuwa instancję modelu
        /// </summary>
        public void Delete()
        {
            MethodInfo saveMethod = MetaModel.ModelType.GetMethod("Delete");
            try
            {
                saveMethod.Invoke(ModelInstance, null);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można usunąć elementu. Powód: " + ex.Message);
            }
        }

        /// <summary>
        /// Zapisuje model
        /// </summary>
        public void Save()
        {
            MethodInfo saveMethod = MetaModel.ModelType.GetMethod("Save");
            try
            {
                saveMethod.Invoke(ModelInstance, null);
            }
            catch (Exception ex)
            {
                throw new Exception("Nie można zapisać elementu. Powód: " + ex.Message);
            }
        }
    }
}
